import { Vector3, Curve ,Matrix4,MathUtils,Quaternion,Line,BufferGeometry,LineBasicMaterial} from "three";

const {sin, cos ,tan,atan, PI} = Math;
/**
 * 螺旋曲线
 */
export class CustomSinCurve extends Curve {
  constructor(turn = 3 , depth = 1,startRadius=0,endRadius=1,axis = 'z') {
    super();
    this.turn = turn;
    this.depth = depth;
    this.axis = axis;
    this.startRadius = startRadius;
    this.endRadius = endRadius;

  }
   getPoint (t, target = new Vector3()) {
      // x y 分别取正余弦 可不就是圆
      let z = t * this.depth;
      let radius = this.startRadius + (this.endRadius - this.startRadius) * t;
      let x =  sin(t * PI * 2 * this.turn)* radius;
      let y =  cos(t * PI * 2 * this.turn)* radius;
      if( this.axis === 'x'){
        [x, y, z] = [z, y, -x] ;

      }
      if( this. axis === 'y'){
        [x, y, z] = [x, z, -y] ;
        
      }
      return target.set(x, y, z);
    }
    getPoint0 (t, target = new Vector3()) {
      // x y 分别取正余弦 可不就是圆
      let x = t * sin(t * PI * 2 * this.turn)* this.startRadius;
      let y = t * cos(t * PI * 2 * this.turn)* this.startRadius;
      let z = t * this.depth;
      if( this.axis === 'x'){
        [x, y, z] = [z, y, -x] ;

      }
      if( this. axis === 'y'){
        [x, y, z] = [x, z, -y] ;
        
      }
      return target.set(x, y, z);
    }
    getTangent(t, tangent = new Vector3()){
      const d = 0.0001;
      let t1 = t - d, t2 =  t + d;
      // if( t1 < 0 ) t1 = 0;
      // if( t2 > 1 ) t2 = 1;
      const A = this.getPoint(t1), B = this.getPoint(t2) ;
		  tangent.copy( B ).sub( A ).normalize();
      return tangent;
    }
  // t是0-1的一个值 这个方法就是最终返回顶点
 
}


export class SpiralPath extends Curve {
  hillAngle = 0;
  totalLength = 0;
  cacheArcLengths = [];
  hillDir = new Vector3();
  q1 = new Quaternion();
  q2 = new Quaternion();
  v1 = new Vector3();
  v2 = new Vector3();
  constructor(r = 1, theta = PI * 2, depth = 1, thetaOffset = 0) {
    super();

    this.r = r;
    this.theta = theta;
    this.thetaOffset = thetaOffset;
    this.depth = depth;

    const h = this.depth * (this.theta / PI * 0.5); // 一圈对应的高度
    const angle = atan(h / (this.r* PI * 2));
    this.hillAngle = angle; // 就是坡度
    // 总长就是高度除以坡度 乘以圈数
    this.totalLength = this.depth / sin(this.hillAngle) * theta/PI * 0.5;
    // 旋转角度为0  半径方向为x轴， 切线方向是 负Z
    const tangent = new Vector3(0, tan(this.hillAngle), -1);
    this.hillDir = tangent.normalize();
  }

  getPoint(t, target = new Vector3()) {
    const y = this.depth * t;
    const theta = this.theta * t + this.thetaOffset;
    return target.set(this.r * Math.cos(theta), y, this.r * -Math.sin(theta));
  }
  //  getTangent(t,target = new Vector3()){
  //     //  切线就是平面圆的切线， 然后绕其半径旋转一定角度，这个一定角度用路径总长和高度可以计算出来
  //     // 这里应该取一圈对应的长度，然后高度取一圈对应的高度 但是还是算错了
  //     // 最新思路， 先用角度算出斜线的方向，然后绕Y轴旋转它 ， 这样才是真的保证坡度一致的切线

  //     const theta = this.theta * t + this.thetaOffset;//
  //     // 初始切线就是连线半径 绕y旋转90  （z， 0，-x）
  //     /*  target.set( -sin(theta), 0, -cos(theta) );
  //     const q = this.q1.setFromAxisAngle( new Vector3(cos( theta), 0, -sin(theta)), this.hillAngle );
  //     target.applyQuaternion(q).normalize()
  //      */

  //     target.copy(this.hillDir);
  //     target.applyQuaternion(this.q1.setFromAxisAngle(new Vector3(0, 1, 0), theta))
  //     target.normalize()

  //     return target

  // }
  getLength() {
    return this.totalLength;
  }
  getLengths(divisions = 10) {
    if (this.cacheArcLengths.length) return this.cacheArcLengths;
    // 计算前缀和

    // this.cacheArcLengths.push(0) ;
    for (let index = 0; index <= divisions; index++) {
      this.cacheArcLengths.push((this.totalLength * index) / divisions);
    }
    return this.cacheArcLengths;
  }
}

//  想实现一个绕曲线缠绕的曲线
class SpiralPath2 extends Curve {
  hillAngle = 0;
  totalLength = 0;
  cacheArcLengths = [];

  q1 = new Quaternion();
  q2 = new Quaternion();
  v1 = new Vector3();
  v2 = new Vector3();
  constructor(
    r = 1,
    theta = PI * 2,
    depth = 1,
    curve = new CatmullRomCurve3()
  ) {
    super();

    this.r = r;
    this.theta = theta;
    this.depth = depth;

    const h = ((this.depth * this.theta) / PI) * 0.5; // 一圈对应的高度
    const angle = atan((h / this.r) * PI * 2);
    this.hillAngle = angle; // 就是坡度
    this.curve = curve;
    // this.totalLength = this.depth / sin(this.hillAngle);
  }
  //  现在取点的规则就是先从curve中取点，然后再绕其切线旋转一定角度
  getPoint(t) {
    const p = this.curve.getPoint(t);
    const tangent = this.curve.getTangent(t);
    // 只要和法线垂直肯定是在那个平面上，但是还是挺难确定的啊。看来最佳办法就是叉乘了，但是胡乱叉乘的话，又如何保证旋转角度一直递增呢。
    const y = this.depth * t;
    const theta = this.theta * t;
    return new Vector3(this.r * Math.cos(theta), y, this.r * -Math.sin(theta));
  }
  /*  getTangent(t,target = new Vector3()){
        //  切线就是平面圆的切线， 然后绕其半径旋转一定角度，这个一定角度用路径总长和高度可以计算出来
        // 这里应该取一圈对应的长度，然后高度取一圈对应的高度 但是还是算错了


        const theta = this.theta * t;// 
        // 初始切线就是连线半径 绕y旋转90  （z， 0，-x）
         target.set( -sin(theta), 0, -cos(theta) );
        const q = this.q1.setFromAxisAngle( new Vector3(cos( theta), 0, -sin(theta)), this.hillAngle );
        target.applyQuaternion(q).normalize()
        return target


    } */
  getLength() {
    return this.totalLength;
  }
  getLengths(divisions = 10) {
    if (this.cacheArcLengths.length) return this.cacheArcLengths;
    // 计算前缀和

    // this.cacheArcLengths.push(0) ;
    for (let index = 0; index <= divisions; index++) {
      this.cacheArcLengths.push((this.totalLength * index) / divisions);
    }
    return this.cacheArcLengths;
  }
}


/*要给tube 使用 只要实现 computeFrenetFrames 方法 也就是只要实现 getPoint getTangent

与Line3不同的地方，多了直接取关键点的选项，拐角处的切线取角平分线的垂线，线段中间的切线取线段的方向。
 todo  处理close的情况
*/
/**
 *  折线
 */
export class PolyLineCurve3 extends Curve {
	v1 = new Vector3();
	v2 = new Vector3();
  /**
   * 
   * @param {Vector3[]} points 
   * @param {boolean} closed 
   * @param {number} radius 补点距离， 0则不补点 
   */
	constructor(points = [new Vector3(), new Vector3()], closed = false, radius =0) {
    super();
    this.isLineCurve3 = true;
    this.type = "LineCurve3";
    // 目前看来就是加点最方便，不然距离计算有问题
    if(closed){ points.push(points[0]) } 
	 //  折线管道，其uv分布会不均匀，需要在每个点附近加两个点，以保证在至少直线段上的uv分布是均匀，只在拐角处扭曲。
   if( radius){

     const result = [points[0]] ;
     for (let index = 1; index <points.length -1 ; index++) {
       const A = points[index -1 ] ,  B = points[index], C = points[index + 1];
       this.v1.subVectors( B, A );
       this.v2.subVectors( C, B );
       const angle = this.v1.angleTo(this.v2);
       if( angle < 0.001  ){ continue }
       //  这个角等于是拐角的补角， 还是得用正切，这个连线的距离不知道啊
       const  offset =  radius * Math.tan( angle  * .5 ) ;
       
       // 要么算距离，要么算法线，少不了的
       this.v1.normalize();
       this.v2.normalize();
       const p1 = this.v1.clone().multiplyScalar( -offset).add(B) ;
       const p2 = this.v2.clone().multiplyScalar( offset).add(B) ;
       result.push(p1,B,p2);
       
      }
      result.push(points[points.length -1]);
      this.points = result;
    }else {
      this.points = points;
    }


    this.radiusArr = new Array(this.points.length).fill(1);
    this.closed = closed;
    this.cacheArcLengths = [];
    this.percents = [];
    this.getLengths();

    // 原方法是依赖getpoint，我只是要计算折线的长度而已 计算前缀和 以便确定要获取的点应该落在哪两个点之间
  }

  getLengths() {
    if (this.cacheArcLengths.length > 0) return this.cacheArcLengths; 
    let sum = 0;
	  this.cacheArcLengths.push(0);
    for (let index = 0; index < this.points.length - 1; index++) {
      const p0 = this.points[index];
      const p1 = this.points[index + 1];
      sum += p0.distanceTo(p1);
      this.cacheArcLengths.push(sum);
    }
    for (let index = 0; index < this.cacheArcLengths.length - 1; index++) {
      this.percents.push(this.cacheArcLengths[index] / sum);
    }
    // 以防误差
    this.percents.push(1);

    return this.cacheArcLengths;
  }
  /**曲线半径 用于曲线转网格 外界可以直接覆盖这个函数，就能自定义了达到和几何节点差不多的效果*/
  getRadius(){
    return this.radiusArr;
  }
  // 设置某个点的半径 暂未实现
  setRadius(alpha ,radius){

    return this;
  }

  getPoint(t, optionalTarget = new Vector3()) {
      // 考虑close的情况 直接取余
      if (this.closed) {
        t = t % 1;
        if(t < 0) t = 1 + t;
      }
    const point = optionalTarget;
    const l = this.points.length;
    //   查找区间  找到第一个大于他或者等于的 零 是得特殊处理
    const endInd = this.percents.findIndex((per) => per >= t);
    const startInd = endInd - 1;

    let weight =
      (t - this.percents[startInd]) /
      (this.percents[endInd] - this.percents[startInd]);
      if(!this.points[startInd]) {debugger};

    if (t === 1) {
      point.copy(this.points[l - 1]);
    } else if (t === 0) {
      point.copy(this.points[0]);
    } else {
      point.copy(this.points[endInd]).sub(this.points[startInd]);
      point.multiplyScalar(weight).add(this.points[startInd]);
    }

    return point;
  }
  // 我该写这个目的就是要取到关键点，所以这里的broken默认为true， 如果有其他地方确实需要取线段上的任一点，就应该改参数 
  getPointAt(u, optionalTarget, broken = true) {
	if (broken) {
		return  optionalTarget.copy( this.points[this.closed? u:u%this.points.length]);
	  }
    return this.getPoint(u, optionalTarget);
  }
  //  切线 这里只处理非关键点，关键点应该用下面的方法
  getTangent(t, optionalTarget = new Vector3(),broken= false) {
    if(this.closed){ t = t % 1;
      if(t < 0) t = 1 + t;
    }
    if (t === 0)
      return optionalTarget
        .subVectors(this.points[1], this.points[0])
        .normalize();
  if( broken) return this.getTangentAt(t, optionalTarget);
    const endInd = this.percents.findIndex((per) => per >= t);
    const startInd = endInd - 1;

    return optionalTarget
      .subVectors(this.points[endInd], this.points[startInd])
      .normalize();
  }

  //  改成取角平分线
  getTangentAt(u = 0, optionalTarget = new Vector3()) {
    let last = this.points[u - 1],
      m = this.points[u],
      next = this.points[u + 1];
    if (u - 1 < 0) {
      return next.clone().sub(m).normalize();
    }
    if (u + 1 >= this.points.length) {
      return m.clone().sub(last).normalize();
    }
    //  这里要用三次单位化，看看有没有其他办法。
    this.v1.subVectors(m, last).normalize();
    this.v2.subVectors(next, m).normalize();
    return optionalTarget.addVectors(this.v1, this.v2).normalize();
  }

  //  这个得改成取角平分线的垂线。 segments在这里无意义了  segments 得另外写一个逻辑，只在每一截线段里面细分
  computeFrenetFrames(segments = 1, closed = false) {
    // 对于折线来说，我不允许重采样 
    segments = this.points.length - 1;
    // see http://www.cs.indiana.edu/pub/techreports/TR425.pdf

    const normal = new Vector3();

    const tangents = [];
    const normals = [];
    const binormals = [];

    const vec = new Vector3();
    const mat = new Matrix4();

    // compute the tangent vectors for each segment on the curve

    for (let i = 0; i <= segments; i++) {
		
		//   const u = i / segments;
      tangents[i] = this.getTangentAt(i, new Vector3());
    }

    // select an initial normal vector perpendicular to the first tangent vector,
    // and in the direction of the minimum tangent xyz component

    normals[0] = new Vector3();
    binormals[0] = new Vector3();
    let min = Number.MAX_VALUE;
    const tx = Math.abs(tangents[0].x);
    const ty = Math.abs(tangents[0].y);
    const tz = Math.abs(tangents[0].z);

    if (tx <= min) {
      min = tx;
      normal.set(1, 0, 0);
    }

    if (ty <= min) {
      min = ty;
      normal.set(0, 1, 0);
    }

    if (tz <= min) {
      normal.set(0, 0, 1);
    }

    vec.crossVectors(tangents[0], normal).normalize();

    normals[0].crossVectors(tangents[0], vec);
    binormals[0].crossVectors(tangents[0], normals[0]);

    // compute the slowly-varying normal and binormal vectors for each segment on the curve

    for (let i = 1; i <= segments; i++) {
      normals[i] = normals[i - 1].clone();

      binormals[i] = binormals[i - 1].clone();

      vec.crossVectors(tangents[i - 1], tangents[i]);

      if (vec.length() > Number.EPSILON) {
        vec.normalize();

        const theta = Math.acos(
          MathUtils.clamp(tangents[i - 1].dot(tangents[i]), -1, 1)
        ); // clamp for floating pt errors

        normals[i].applyMatrix4(mat.makeRotationAxis(vec, theta));
      }

      binormals[i].crossVectors(tangents[i], normals[i]);
    }

    // if the curve is closed, postprocess the vectors so the first and last normal vectors are the same

    if (closed === true) {
      let theta = Math.acos(
        MathUtils.clamp(normals[0].dot(normals[segments]), -1, 1)
      );
      theta /= segments;

      if (
        tangents[0].dot(vec.crossVectors(normals[0], normals[segments])) > 0
      ) {
        theta = -theta;
      }

      for (let i = 1; i <= segments; i++) {
        // twist a little...
        normals[i].applyMatrix4(mat.makeRotationAxis(tangents[i], theta * i));
        binormals[i].crossVectors(tangents[i], normals[i]);
      }
    }

    return {
      tangents: tangents,
      normals: normals,
      binormals: binormals,
    };
  }
  copy(source) {
    super.copy(source);

    this.v1.copy(source.v1);
    this.v2.copy(source.v2);

    return this;
  }
  toJSON() {
    const data = super.toJSON();

    data.v1 = this.v1.toArray();
    data.v2 = this.v2.toArray();

    return data;
  }
  fromJSON(json) {
    super.fromJSON(json);

    this.v1.fromArray(json.v1);
    this.v2.fromArray(json.v2);

    return this;
  }
}



export class HeartCurve extends Curve {
  constructor(size =1) {
    super();
    this.size = size;
   }
  getPoint(t, target  = new Vector3()){
    // x = 16sin³t  [-12,12]
// y = 13cos t - 5cos(2t) - 2cos(3t) - cos(4t) [-17,12]
    t *= 2 *PI;
    // 这个运算量有点儿大 拉宽 y小于零的部分收缩 
    let y = 13*cos(t)-5*cos(2*t) -2*cos(3*t)- cos(4*t);
    // y * ( y<0 ?.8 : 1);
    return target.set(this.size * 16 *  sin(t) ** 3,y * this.size ,0);
    // return target.set( sin(t) ** 3, cos(t)-cos(t) ** 4 ,0);
  }
  // 为了取消原本 01的限制
  getTangent(t, tangent = new Vector3()){
    const d = 0.0001;
    let t1 = t - d, t2 =  t + d;
    // if( t1 < 0 ) t1 = 0;
    // if( t2 > 1 ) t2 = 1;
    const A = this.getPoint(t1), B = this.getPoint(t2) ;
    tangent.copy( B ).sub( A ).normalize();
    return tangent;
  }
}

export class CircleCurve extends Curve {
  /**
 * 构造函数
 *
 * @param {number} r - 圆的半径，默认为1
 * @param {Vector3} center - 圆心位置，默认为(0,0,0)
 * @param {number} startAngle - 起始角度，默认为0
 */
  constructor(r = 1, center = new Vector3(0,0,0), startAngle = 0,axis = 'z'){
    super();
    this.r = r;
    this.center = center;
    this.startAngle = startAngle;
    this.closed = true;
    this.axis = axis;
  }
  getPoint(t, target  = new Vector3()){
    t *= 2*PI + this.startAngle; 
    let x = cos(t)*this.r, y = sin(t) *this.r;
    if( this.axis === 'x')  {target.set(0,y,-x);}
    else  if( this.axis === 'y')  {target.set(x,0,-y);}
    else target.set(x,y,0);

    return target.add(this.center);
  }
  getTangent(t, target  = new Vector3()){
    t *= 2*PI+ this.startAngle;
    let y = cos(t)*this.r, x = -sin(t) *this.r;

    if( this.axis === 'x')  {target.set(0,y,-x);}
    else  if( this.axis === 'y')  {target.set(x,0,-y);}
    else target.set(x,y,0);
    return target;
  }
}
/**
 * 椭圆曲线
 */
export class EllipseCurve extends Curve {
  constructor(radiusX = 1, radiusY = 1, aStartAngle = 0, aEndAngle = Math.PI * 2, aClockwise = false) {
    super();
    this.radiusX = radiusX;
    this.radiusY = radiusY;
    this.aStartAngle = aStartAngle;
    this.aEndAngle = aEndAngle;
    this.aClockwise = aClockwise;
  }
  getPoint(t, target  = new Vector3()){
    let angle = this.aStartAngle + t * (this.aEndAngle - this.aStartAngle);
    if (this.aClockwise) angle = this.aEndAngle - angle;
    target.set(
      this.radiusX * Math.cos(angle),
      this.radiusY * Math.sin(angle),
      0
    );
    return target;
  }
  
}

/**
 * 一笔画交错的五角星 只能是奇数，大于3的多角星
 */
export class FiveStarCurve extends PolyLineCurve3 {
  constructor(radius = 1, startAngle = 0,n = 5) {
    const points = [];
    let angle = 0;
    for (let i = 0; i < n; i++) {
      // 一笔画五角星 间隔1取点 刚好就是 2*i，这里又是正余弦，不用取余了。
      angle = (2*i / n) * PI*2 + startAngle;
      // angle = (i * 0.8) * PI + startAngle;
      points.push(new Vector3(sin(angle) * radius, cos(angle) * radius, 0));
    }
    super(points , true, radius*0.01);
}
}
/**
 *  空心多角星 n大于3
 */
export class starCurve extends PolyLineCurve3 {
  constructor(inRadius = .5, outRadius =1,startAngle = 0,n = 4) {
    const points = [];
    let angle = 0,angle2 = 0;
    for (let i = 0; i < n; i++) {
      // 一笔画五角星 间隔1取点 刚好就是 2*i，这里又是正余弦，不用取余了。
      angle = (i / n) * PI*2 + startAngle;
      angle2 = ((i +.5) / n) * PI*2 + startAngle;
      // angle = (i * 0.8) * PI + startAngle;
      points.push(new Vector3(sin(angle) * outRadius, cos(angle) * outRadius, 0,),
                  new Vector3(sin(angle2) * inRadius, cos(angle2) * inRadius, 0)
      );
    }
    super(points , true, 0);
}
}

/**就是一条竖线下来， 然后再上来，完全重合的，然后再斜向上兜圈子回到起点
 * 曲线半径的变化，竖线下的时候，越来越大，快触底的时候又变小，上升的时候继续变小
 * 后面基本不变了
 */
class KleinBottleCurve extends Curve  {
  constructor(height=4,maxR = 1,){
    super();
    this.height = height;
    
  }
  getPoint(t, target  = new Vector3()){
    // 四六分吧
    if( t < 0.4){
      return target.set(0,(1-t*2.5)*this.height,0);
    }
    if( t < 0.6){
      // 上升的直线段就咱且设为20%吧
      return target.set(0,this.height*(t-0.4),0);
    }
    // 这一段理论上 最好就来一个贝塞尔曲线吧
    // 画圆，控制圆的半径 随角度变化
    let angle = t*2*PI;
    let left = -2;
    let r = 1 + sin(t) * sin(t);
    // 画直线
    if( t< 0.125 || t > 0.875){
      r= left/ cos(angle)
    }else if(t<0.5){
      r= 1
    }else {
      // 慢慢变大
      r= t*2 + 1
    }
    const x = r * cos(t);
    const y = r * sin(t);
    const z = sin(t) * cos(t);
    return target.set(x,y,z);
  }
  getRadius(u){
    return  ( 1 - Math.cos( u ) / 2 ) 
  }
  
}

export class CurveLine extends Line {
  constructor(curve, divisions = 100, color = 0xffffff) {
    
    const points  =curve.getPoints(divisions) ;
    if( curve.closed){ 
      points.push(points[0]);
    }
    const geo = new BufferGeometry().setFromPoints(points) ;
    const mat  = new LineBasicMaterial({color}) ;
    super(geo, mat) ; 
    this.type = "CurveLine";
    this.curve = curve;
  }
}